之前在Android 就接觸過MVC、MVP以及MVVM,這邊先不對各差別去作比較分析,直接來對MVVM 做個簡單的介紹
MVVM 比 MVC 模型多添加了一個組件,稱為 ViewModel(視圖模型),它可以是 class 或 struct,但通常會是一個 class,因此可以在程式碼中傳遞同一個物件的 reference,而 ViewModel 就位於視圖控制器和模型之間,其中這個模式的核心是ViewModel,它是一種特殊的model型別,用於表示程式的UI狀態。它包含描述每個UI控制元件的狀態的屬性。
在最簡單的情況下,View 不依賴於任何外部狀態,它的本地 @State 變量扮演 ViewModel 的角色,提供訂閱機制(綁定)以在狀態更改時刷新 UI。
對於更複雜的場景,視圖可以引用外部 ObservableObject,在這種情況下可以是不同的 ViewModel。
無論如何,SwiftUI 視圖處理狀態的方式非常類似於經典的 MVVM(除非我們引入更複雜的編程實體圖)。
還有很多關於 Clean Architecture and MVVM on iOS 的內容,但這邊就不做詳細介紹
推薦文章:
Clean Architecture for SwiftUI
Clean Architecture and MVVM on iOS
這邊做一個簡單的MVVM 架構的Core Data 專案
首先在Xcode 建立一個App 專案CoreDataSample,接下來新增Data Model,所建立的ManagedObjectModel會實例化PersistentStoreCoordinator,讓PersistentStoreCoordinator知道應用程序的類型、屬性和關係
選擇建立Data Model:
輸入你的名稱後按create 按鈕建立
產生Persons.xcdatamodeld後,選取它後建立Core Data Model 的實體(Entity)
並設定Entity 名稱與其屬性與關聯性
接下來我們建立一個CoreDataManager.swift來準備建立NSPersistentContainer 的實例,之後會透過單例去管理對Core Data 的任何存取等動作,首先初始化persistentContainer需要透過loadPersistentStores(NSPersistentStoreDescription)來指定container載入前面建立的Persistent Stores 與建立Core Data stack,如果載入資料時發生錯誤則會生出一個NSError的值
import Foundation
import CoreData
class CoreDataManager {
    let persistentContainer: NSPersistentContainer
    static let shared = CoreDataManager()
 		var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    private init() {
        persistentContainer = NSPersistentContainer(name: "Persons")
        persistentContainer.loadPersistentStores { (description, error) in
            if let error = error {
                fatalError("Unable to initialize Core Data Stack \(error)")
            }
        }
    }
}
接下來建立一個PersonListViewModel來讓View 層透過此ViewModel來取得所需要的資料,為此我們需要先在CoreDataManager新增對資料庫存取的方法以供PersonListViewModel調用
class CoreDataManager {
  	//Net Added
    func getPersonById(id: NSManagedObjectID) -> PersonItem? {
        do {
            return try viewContext.existingObject(with: id) as? PersonItem
        } catch {
            return nil
        }
    }
    func deletePerson(person: PersonItem) {
        viewContext.delete(person)
        save()
    }
    func getAllPersons() -> [PersonItem] {
        let request: NSFetchRequest<PersonItem> = PersonItem.fetchRequest()
        do {
            return try viewContext.fetch(request)
        } catch {
            return []
        }
    }
    func save() {
        do {
            try viewContext.save()
        } catch {
            viewContext.rollback()
            print(error.localizedDescription)
        }
    }
    // ...
然後建立PersonListViewModel
import Foundation
import CoreData
class PersonListViewModel: ObservableObject {
    var name: String = ""
    var height: String = ""
    var weight: String = ""
    @Published var persons: [PersonViewModel] = []
    func getAllPersons() {
        persons = CoreDataManager.shared.getAllPersons().map(PersonViewModel.init)
    }
    func delete(_ person: PersonViewModel) {
        let existingPerson = CoreDataManager.shared.getPersonById(id: person.id)
        if let existingPerson = existingPerson {
            CoreDataManager.shared.deletePerson(person: existingPerson)
        }
    }
    func save() {
        let person = PersonItem(context: CoreDataManager.shared.viewContext)
        person.name = name
        person.height = (height as NSString).floatValue
        person.weight = (weight as NSString).floatValue
        CoreDataManager.shared.save()
    }
}
struct PersonViewModel {
    let personItem: PersonItem
    var id: NSManagedObjectID {
        return personItem.objectID
    }
    var name: String {
        return personItem.name ?? ""
    }
    var height: Float {
        return personItem.height
    }
    var weight: Float {
        return personItem.weight
    }
}
extension Float {
    var clean: String {
       return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self)
    }
}
最後在View 端調用ViewModel取得資料,並顯示畫面就完成了
import SwiftUI
struct ContentView: View {
    @StateObject private var viewModel = PersonListViewModel()
    var body: some View {
        VStack {
            TextField("Enter person name", text: $viewModel.name)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            TextField("Enter person height", text: $viewModel.height)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .keyboardType(.numberPad)
            TextField("Enter person weight", text: $viewModel.weight)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .keyboardType(.numberPad)
            Button("Save") {
                viewModel.save()
                viewModel.getAllPersons()
            }
            List {
                ForEach(viewModel.persons, id: \.id) { person in
                    Text("name: \(person.name), h: \(person.height.clean), w: \(person.weight.clean)")
                }.onDelete(perform: deletePerson)
            }
            Spacer()
        }.padding()
        .onAppear(perform: {
            viewModel.getAllPersons()
        })
    }
    func deletePerson(at offsets: IndexSet) {
        offsets.forEach { index in
            let task = viewModel.persons[index]
            viewModel.delete(task)
        }
        viewModel.getAllPersons()
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
這邊也附上程式碼 Github: CoreDataSample